package com.xsystem.api.util;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xsystem.orm.user.User;
import com.xsystem.util.StringUtil;

public class ApiMonitor {

	private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss";
	private static final String LOG_NAME = "XLogger";

	private Date startTime;
	private String uri;
	private String requestIp;
	private Map<String, String> inputParams;
	private Map<String, Object> outputParams;
	private Map<String, String> transactions; // Name of transaction vs transaction reference value
	private StringBuilder message;
	private StringBuilder developerLog;
	private List<ExceptionLog> exceptionLogList;
	private List<VendorLog> vendorLogList;
	private String user;
	private Logger developerLogger;
	private String appId;

	private static final Logger defaultLog = LoggerFactory.getLogger(LOG_NAME);
	private static final Logger apiMonitorLog = LoggerFactory.getLogger(ApiMonitor.class);

	public ApiMonitor() {
		transactions = new TreeMap<String, String>();
		exceptionLogList = new ArrayList<ExceptionLog>();
		vendorLogList = new ArrayList<VendorLog>();
		message = new StringBuilder();
		user = "";
	}

	public void begin() {
		startTime = new Date();
	}

	public void setRequestIp(String requestIp) {
		this.requestIp = requestIp;
	}

	public void setUri(String uri) {
		this.uri = uri;
	}

	public void setInputParams(Map<String, String> inputParams) {
		this.inputParams = inputParams;
	}

	public String getInputParamsAsString() {
		return JSONObject.fromObject(inputParams).toString();
	}

	public void setOutputParams(Map<String, Object> outputParams) {
		this.outputParams = outputParams;
	}

	public void setDeveloperLogger(Logger developerLogger) {
		this.developerLogger = developerLogger;
	}

	public void addTransaction(String name, String txnref) {
		transactions.put(name, txnref);
	}

	public String getAppId() {
		return appId;
	}

	public void setAppId(String appId) {
		this.appId = appId;
	}

	public void appendDeveloperLog(String message) {
		if (developerLog == null) {
			developerLog = new StringBuilder();
			developerLog.append(message);
		} else {
			developerLog.append("\n");
			developerLog.append(message);
		}
	}

	public void appendDeveloperException(Exception e, boolean toPrintStackTrace) {
		exceptionLogList.add(new ExceptionLog(e, toPrintStackTrace));
	}

	public void appendMessage(String message) {
		this.message.append(message);
		this.message.append("\r\n");
	}

	public void vendorBeginLog(String destination, String input) {
		VendorLog vendorLog = new VendorLog();
		vendorLog.setStartTime(new Date());
		vendorLog.setDestination(destination);
		vendorLog.setInput(input);
		vendorLogList.add(vendorLog);
	}

	public void vendorEndLog(String output) {
		if (!vendorLogList.isEmpty()) {
			VendorLog vendorLog = vendorLogList.get(vendorLogList.size() - 1);
			vendorLog.setOutput(output);
			vendorLog.setEndTime(new Date());
		}
	}

	public void setUser(User user) {
		if (user != null) {
			this.user = user.getId().toString();
		}
	}

	public void setUser(String user) {
		this.user = user;
	}

	public void record() {
		long timeTaken = new Date().getTime() - startTime.getTime();
		String hostName = null;
		try {
			hostName = InetAddress.getLocalHost().toString();
		} catch (UnknownHostException e) {
			hostName = "failed to get local host";
		}

		recordApiLog(timeTaken, hostName);
		recordDeveloperLog(timeTaken);
	}

	private void recordApiLog(long timeTaken, String hostName) {

		SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);

		// List for CSV. Order is important here.
		List<String> list = new ArrayList<String>();
		list.add(sdf.format(startTime));
		list.add(uri);
		list.add(JSONObject.fromObject(inputParams).toString());
		if (outputParams == null) {
			list.add("(omitted)");
		} else {
			list.add(JSONObject.fromObject(outputParams).toString());
		}
		list.add(user);
		list.add(JSONObject.fromObject(transactions).toString());
		list.add(this.constructApiLogMessage(timeTaken));
		list.add(hostName);
		list.add(requestIp);
		list.add(Long.toString(timeTaken));

		String csv = StringUtil.convertListToCsv(list);

		apiMonitorLog.info(csv);
	}

	private String constructApiLogMessage(long executionTime) {
		JSONObject json = new JSONObject();
		if (message != null && message.length() > 0) {
			json.put("msg", message.toString());
		}
		if (developerLog != null && developerLog.length() > 0) {
			json.put("log", developerLog.toString());
		}

		if (!exceptionLogList.isEmpty()) {
			JSONArray jsonArray = new JSONArray();
			for (ExceptionLog exceptionLog : exceptionLogList) {
				Exception e = exceptionLog.getException();
				String msg = e.getClass() + (e.getMessage() != null ? " :: " + e.getMessage() : "");
				jsonArray.add(msg);
			}
			json.put("err", jsonArray);
		}

		if (!vendorLogList.isEmpty()) {
			long vendorTime = 0;
			JSONArray jsonArray = new JSONArray();
			for (VendorLog vendorLog : vendorLogList) {
				JSONObject vendorJ = new JSONObject();
				vendorJ.put("url", vendorLog.getDestination());
				vendorJ.put("time", vendorLog.getTimeTaken());
				vendorJ.put("in", vendorLog.getInput());
				vendorJ.put("out", vendorLog.getOutput());
				jsonArray.add(vendorJ);

				vendorTime += vendorLog.getTimeTaken();
			}
			json.put("vendor", jsonArray);
			json.put("v_time", vendorTime);
			json.put("a_time", executionTime - vendorTime);
		}

		return json.toString();
	}

	private void recordDeveloperLog(long timeTaken) {

		Logger log = getDeveloperLogger();

		log.info("============BEGIN REQUEST: {} :: {} ==========", uri, requestIp);
		log.info("[INPUT]: {}", inputParams);
		log.info("[OUTPUT]: {}", outputParams);

		if (developerLog != null && developerLog.length() > 0) {
			log.info("[LOG]: {}", developerLog.toString());
		}

		for (ExceptionLog exceptionLog : exceptionLogList) {
			Exception e = exceptionLog.getException();
			String msg = e.getClass() + (e.getMessage() != null ? " :: " + e.getMessage() : "");
			if (exceptionLog.isToPrintStackTrace()) {
				log.error("[EXCEPTION]: " + msg, e);
			} else {
				log.error("[EXCEPTION]: " + msg);
			}
		}

		for (VendorLog vendorLog : vendorLogList) {
			log.info("[VENDOR]");
			log.info("\tdestination: {}", vendorLog.getDestination());
			log.info("\ttimeTaken: {}", Long.toString(vendorLog.getTimeTaken()));
			log.info("\tinput: {}", vendorLog.getInput());
			log.info("\toutput: {}", vendorLog.getOutput());
		}

		log.info("============END REQUEST: {} :: {} ms ==========", uri, timeTaken);
	}

	private Logger getDeveloperLogger() {
		if (developerLogger == null) {
			return defaultLog;
		} else {
			return developerLogger;
		}
	}

	private static class ExceptionLog {
		private boolean toPrintStackTrace;
		private Exception exception;

		public ExceptionLog(Exception exception, boolean toPrintStackTrace) {
			super();
			this.exception = exception;
			this.toPrintStackTrace = toPrintStackTrace;
		}

		public boolean isToPrintStackTrace() {
			return toPrintStackTrace;
		}

		public Exception getException() {
			return exception;
		}
	}

	private static class VendorLog {

		private Date startTime;
		private Date endTime;
		private String input;
		private String output;
		private String destination;

		public VendorLog() {
		}

		public void setStartTime(Date startTime) {
			this.startTime = startTime;
		}

		public Date getStartTime() {
			return startTime;
		}

		public void setEndTime(Date endTime) {
			this.endTime = endTime;
		}

		public Date getEndTime() {
			return endTime;
		}

		public void setInput(String input) {
			this.input = input;
		}

		public String getInput() {
			return input;
		}

		public void setOutput(String output) {
			this.output = output;
		}

		public String getOutput() {
			return output;
		}

		public void setDestination(String destination) {
			this.destination = destination;
		}

		public String getDestination() {
			return destination;
		}

		public long getTimeTaken() {
			if (this.getEndTime() == null) {
				this.setEndTime(new Date());
			}
			return this.getEndTime().getTime() - this.getStartTime().getTime();
		}

	}

}
